/*
 * MIT License
 * 
 * Copyright (c) 2024-2025 milkpotatoes
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include "include/js_textdecoder.h"

napi_value JsTextDecoder::Init(napi_env env, napi_value global)
{
    std::vector<napi_property_descriptor> desc{
        {"_innerBinding", nullptr, Binding, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
        {"_innerDecode", nullptr, Decode, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr},
    };
    napi_value decoderUtils = utils::CreateObject(env);
    napi_define_properties(env, decoderUtils, desc.size(), desc.data());

    napi_set_named_property(env, global, "_innerDecoderUtils", decoderUtils);
    return decoderUtils;
}

#define FAILED_TO_CONSTRUCT "Failed to construct 'TextDecoder': "
JsTextDecoder *JsTextDecoder::New(napi_env env, const std::string &encoding, bool fatal)
{
    JsTextDecoder::ConvertNessType type = JsTextDecoder::ConvertNessType::OTHER;
    UConverter *converter = nullptr;
    UConverter *utf8Converter = nullptr;
    if (!fatal && encoding == "utf-8")
    {
        type = JsTextDecoder::ConvertNessType::UTF8;
    }
    else if (!fatal && encoding == "utf-16")
    {
        type = JsTextDecoder::ConvertNessType::UTF16;
    }
    else
    {
        type = JsTextDecoder::ConvertNessType::OTHER;
        UErrorCode status = U_ZERO_ERROR;
        converter = ucnv_open(encoding.c_str(), &status);
        if (U_FAILURE(status))
        {
            napi_throw_range_error(env, nullptr, (std::string(FAILED_TO_CONSTRUCT) + u_errorName(status)).c_str());
            return nullptr;
        }
        if (fatal)
        {
            ucnv_setToUCallBack(converter, UCNV_TO_U_CALLBACK_STOP, nullptr, nullptr, nullptr, &status);
            if (U_FAILURE(status))
            {
                napi_throw_range_error(env, nullptr, (std::string(FAILED_TO_CONSTRUCT) + u_errorName(status)).c_str());
                return nullptr;
            }
        }
        utf8Converter = ucnv_open("utf-8", &status);
        if (U_FAILURE(status))
        {
            napi_throw_range_error(env, nullptr, (std::string(FAILED_TO_CONSTRUCT) + u_errorName(status)).c_str());
            return nullptr;
        }
    }
    auto encoder = new JsTextDecoder(converter, utf8Converter, type, fatal);
    if (encoder == nullptr)
        napi_throw_range_error(env, nullptr, FAILED_TO_CONSTRUCT "Failed to allocate native binding memory.");
    return encoder;
}

DEF_NAPI_CALLBACK(JsTextDecoder::Binding, info)
{
    napi_env env = info.GetEnv();
    std::string encoding = utils::GetValueStringUtf8(env, info[1]);
    bool throwOnError = utils::GetValueBool(env, info[2]); // 2: index of arg fatal
    auto decoder = New(env, encoding.c_str(), throwOnError);
    if (decoder == nullptr)
        return nullptr;
    if (!utils::Wrap(env, info[0], decoder, Finalize))
    {
        napi_throw_range_error(env, nullptr, FAILED_TO_CONSTRUCT "failed to wrap with object");
        decoder->Delete();
        return nullptr;
    }
    return nullptr;
}
#undef FAILED_TO_CONSTRUCT

DEF_NAPI_CALLBACK(JsTextDecoder::Decode, info)
{
#define FAILED_TO_EXECUTE "Failed to execute 'decode' on 'TextDecoder': "
    napi_env env = info.GetEnv();
    auto that = Unwrap(env, info[0], FAILED_TO_EXECUTE);
    napi_value buffer = info[1];
    void *data = nullptr;
    size_t length;
    if (utils::IsArrayBuffer(env, buffer))
    {
        if (napi_get_arraybuffer_info(env, buffer, &data, &length) != napi_ok)
        {
            napi_throw_range_error(env, nullptr, FAILED_TO_EXECUTE "cannot get buffer data from ArrayBuffer.");
            return nullptr;
        }
    }
    else if (utils::IsTypedArray(env, buffer))
    {
        // length is already byte_length on arkui_napi
        if (napi_get_typedarray_info(env, buffer, nullptr, &length, &data, nullptr, nullptr) != napi_ok)
        {
            napi_throw_range_error(env, nullptr, FAILED_TO_EXECUTE "cannot get buffer data from TypedArray.");
            return nullptr;
        }
    }
    else if (utils::IsDataView(env, buffer))
    {
        if (napi_get_dataview_info(env, buffer, &length, &data, nullptr, nullptr) != napi_ok)
        {
            napi_throw_range_error(env, nullptr, FAILED_TO_EXECUTE "cannot get buffer data from DataView.");
            return nullptr;
        }
    }
    else
    {
        napi_throw_type_error(env, nullptr, FAILED_TO_EXECUTE "parameter 1 is not of type 'ArrayBuffer'.");
        return nullptr;
    }

    switch (that->type_)
    {
    case JsTextDecoder::ConvertNessType::UTF8:
        return utils::CreateStringUtf8(env, static_cast<char *>(data), length);
    case JsTextDecoder::ConvertNessType::UTF16:
        return utils::CreateStringUtf16(env, static_cast<char16_t *>(data), length * sizeof(char) / sizeof(char16_t));
    default:
        break;
    }
    bool success = false;
    std::string errorInfo;
    std::vector<char> result = that->Decode(static_cast<char *>(data), length, success, errorInfo);
    if (success)
        return utils::CreateStringUtf8(env, result.data(), result.size());
    if (that->throwOnError_)
    {
        std::string errMsg = "Failed to Execute 'decode' on 'JsTextDecoder': ";
        errMsg += errorInfo;
        napi_throw_type_error(env, nullptr, errMsg.c_str());
    }
    return nullptr;
}

JsTextDecoder *JsTextDecoder::Unwrap(napi_env env, napi_value object, const char *msg)
{
    JsTextDecoder *that = reinterpret_cast<JsTextDecoder *>(utils::Unwrap(env, object));
    if (that == nullptr)
    {
        std::string errMsg(msg);
        errMsg += ": cannot unwrap native data.";
        utils::ThrowInvalidArg(env, errMsg.c_str());
    }
    return that;
}

JsTextDecoder::JsTextDecoder(UConverter *converter, UConverter *utf8Converter, JsTextDecoder::ConvertNessType type,
                             bool fatal)
    : converter_(converter), utf8Converter_(utf8Converter), type_(type), throwOnError_(fatal)
{}

JsTextDecoder::~JsTextDecoder()
{
    if (converter_ != nullptr)
        ucnv_close(converter_);
    converter_ = nullptr;
}

std::vector<char> JsTextDecoder::Decode(char *buffer, size_t length, bool &success, std::string &errorInfo)
{
    // preallocate buffer
    success = false;
    size_t len = UCNV_GET_MAX_BYTES_FOR_STRING(length, ucnv_getMaxCharSize(utf8Converter_));
    std::vector<char> resultBuffer(len + 1);

    const char *source = buffer;
    char *target = resultBuffer.data();
    UErrorCode status = U_ZERO_ERROR;
    ucnv_convertEx(utf8Converter_, converter_, &target, target + len, &source, buffer + length, nullptr, nullptr,
                   nullptr, nullptr, false, true, &status);
    if (U_FAILURE(status))
    {
        if (throwOnError_)
            errorInfo += u_errorName(status);
        resultBuffer.resize(0);
        return resultBuffer;
    }
    resultBuffer.resize(target - resultBuffer.data());
    success = true;
    return resultBuffer;
}
